/*******************************************************************************
 * Copyright (c) 2023-09-27 @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>.
 * All rights reserved.
 *
 * Contributors:
 *     <a href="mailto:iffiff1@gmail.com">Tyler Chen</a> - initial API and implementation.
 ******************************************************************************/
package org.iff.util;

import org.iff.model.XmlLinkedMap;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * XmlParser：主要用于大 XML 文件解释，并且格式相当规范的。
 *
 * @author <a href="mailto:iffiff1@gmail.com">Tyler Chen</a>
 * @since 2023-09-27
 */
public class XmlParser {
    private TagStartParser tagStartParser = new TagStartParser();// index: -1
    private TagEndParser tagEndParser = new TagEndParser();// index: -2
    private AttrParser attrParser = new AttrParser();// index: -3
    private ValParser valParser = new ValParser();// index: -4
    private AtomicInteger id = new AtomicInteger(1);
    private BufferedReader reader;
    private InnerParser parser;
    private Map<Integer, XmlLinkedMap> idMap = new HashMap<>();
    private XmlLinkedMap root;
    private XmlLinkedMap current;

    public static void main(String[] args) throws Exception {
        {
            String xml = "<?xml version=\"1.0\"?>\n" +
                    "<!DOCTYPE note SYSTEM \"note.dtd\">\n" +
                    "<!--<!DOCTYPE note [\n" +
                    "  <!ELEMENT note (to,from,heading,body)>\n" +
                    "  <!ELEMENT to      (#PCDATA)>\n" +
                    "  <!ELEMENT from    (#PCDATA)>\n" +
                    "  <!ELEMENT heading (#PCDATA)>\n" +
                    "  <!ELEMENT body    (#PCDATA)>\n" +
                    "]>-->\n" +
                    "<note>\n" +
                    "  <to>George</to><!-- comment -->\n" +
                    "  <from>John</from>\n" +
                    "  <heading>Reminder</heading>\n" +
                    "  <body>Don't forget the meeting!</body>\n" +
                    "</note>";
            InputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
            long startTime = System.currentTimeMillis();
            XmlParser xmlParser = new XmlParser(is);
            XmlLinkedMap root = xmlParser.parse();
            long endTime = System.currentTimeMillis();
            System.out.println("====" + (endTime - startTime));
            System.out.println(JSONUtil.toJsonString(root));
            is.close();
        }
    }

    public XmlParser(InputStream is) {
        try {
            this.reader = new BufferedReader(new InputStreamReader(is, "UTF-8"), 8192 * 10/*80k*/);
            init();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    void init() {
        root = createNode(null);
        current = root;
    }

    public XmlLinkedMap parse() throws Exception {
        parser = parser == null ? next() : parser;
        while (parser != null) {
            int ret = parser.parse(reader);
            if (ret == 0) {
                parser = next();
            } else if (ret == -1) {
                parser = tagStartParser.start(parser.remain, parser.node);
                parser = next();
            } else if (ret == -2) {
                parser = tagEndParser.start(parser.remain, parser.node);
                parser = next();
            } else if (ret == -3) {
                parser = attrParser.start(parser.remain, parser.node);
                parser = next();
            } else if (ret == -4) {//tag is closed, next is tag start
                parser = valParser.start(parser.remain, parser.node);
                parser = next();
            }
        }
        if (root.children() != null && root.children().size() > 0) {
            return root = root.children().get(0).pid(null);
        }
        return root;
    }

    XmlLinkedMap createNode(XmlLinkedMap parent) {
        XmlLinkedMap node = new XmlLinkedMap().id(id.getAndIncrement()).attr("@done", false).pid(parent == null ? null : parent.id());
        idMap.put(node.id(), node);
        return node;
    }

    XmlLinkedMap unDone() {
        XmlLinkedMap node = current;
        if (!(Boolean) node.attr("@done")) {
            return node;
        }
        while (node.pid() != null) {
            node = idMap.get(node.pid());
            Boolean isDone = node.attr("@done");
            if (isDone) {
                continue;
            }
            break;
        }
        return node;
    }

    /**
     * [tagNameS - attr - tagNameE - Value - (child) - Value]
     */
    InnerParser next() throws Exception {
        int r = parser == null ? -1 : parser.remain;
        do {
            if (r < 1) {
                continue;
            }
            char c = (char) r;
            if (c == 0 || c == ' ' || c == '\t' || c == '\n' || c == '\r') {
                continue;
            }
            if (parser == null || parser instanceof ValParser) {
                return parser = tagStartParser.start(c, current);
            }
            if (parser instanceof TagStartParser) {
                return parser = attrParser.start(c, current);
            }
            if (parser instanceof AttrParser) {
                return parser = tagEndParser.start(c, current);
            }
            if (parser instanceof TagEndParser) {
                return parser = valParser.start(c, current);
            }
        } while ((r = reader.read()) > -1);
        return null;
    }

    abstract class InnerParser {
        char remain;
        XmlLinkedMap node;

        InnerParser start(char c, XmlLinkedMap node) {
            remain = c;
            this.node = node;
            return this;
        }

        /**
         * return 0: nothing, 1: create new node
         */
        abstract int parse(BufferedReader reader) throws Exception;
    }

    /**
     * <pre>
     * Process:
     * TagOpen: <Tag
     * TagOpenAndClose: <Tag/>
     * </pre>
     */
    class TagStartParser extends InnerParser {
        int parse(BufferedReader reader) throws Exception {
            char remainChar = remain;
            if (remainChar != '<') {
                char nc = nextChar(reader);
                while (true) {// find the tag start
                    if (nc == 0) {
                        return 0;
                    }
                    if (nc == '<') {
                        break;
                    }
                    nc = nextChar(reader);
                }
            }
            remain = 0;
            int r = reader.read(), splash = '/';
            if (r == splash) {//tag close start
                while ((r = reader.read()) > -1) {
                    char c = (char) r;
                    if (c == '>') {//tag close end
                        node.attr("@done", true);
                        ////System.out.println("==tag closed0== </" + node.tag() + ">");
                        node = current = node.pid() == null ? current : idMap.get(node.pid());
                        return -4;//redirect to ValParser
                    }
                }
            }

            StringBuilder sb = new StringBuilder();
            do {
                char c = (char) r;
                while (c == '?' || c == '!') {
                    if (c == '?') {//skip: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
                        c = skipXmlDeclare(reader, c);
                        if (c == 0) {
                            return 0;
                        }
                    }//skip END
                    if (c == '!') {//skip: xml comment: <!-- This is a comment -->
                        c = skipComment(reader, c);
                        if (c == 0) {
                            return 0;
                        }
                    }//skip END
                    if (c == '!') {//skip: DOCTYPE: <!DOCTYPE note SYSTEM "note.dtd">, <!DOCTYPE note []>
                        c = skipDocType(reader, c);
                        if (c == 0) {
                            return 0;
                        }
                    }//skip END
                }
                if (c == 0 || c == ' ' || c == '\t' || c == '\n' || c == '\r') {
                    if (sb.length() < 1) {
                        continue;
                    } else {
                        XmlLinkedMap parent = unDone();
                        node = current = createNode(parent);
                        if (parent.children() == null) {
                            parent.children(new ArrayList<>());
                        }
                        parent.children().add(node);
                        node.tag(sb.toString());
                        ////System.out.println("==tag== <" + sb + ">");
                        break;
                    }
                } else if (c == '/' || c == '>') {
                    XmlLinkedMap parent = unDone();
                    node = current = createNode(parent);
                    if (sb.length() > 0) {
                        if (parent.children() == null) {
                            parent.children(new ArrayList<>());
                        }
                        parent.children().add(node);
                        node.tag(sb.toString());
                    }
                    remain = c;
                    ////System.out.println("==tag== <" + sb + ">");
                    break;
                } else {
                    sb.append(c);
                }
            } while ((r = reader.read()) > -1);
            return 0;
        }

        char skipDocType(BufferedReader reader, char preChar) throws Exception {
            boolean isDocType = false;
            reader.mark(10);
            char nc = nextChar(reader);
            if (nc == 'D') {//DOCTYPE
                nc = nextChar(reader);
                if (nc == 'O') {
                    nc = nextChar(reader);
                    if (nc == 'C') {
                        nc = nextChar(reader);
                        if (nc == 'T') {
                            nc = nextChar(reader);
                            if (nc == 'Y') {
                                nc = nextChar(reader);
                                if (nc == 'P') {
                                    nc = nextChar(reader);
                                    if (nc == 'E') {// is xml DOCTYPE
                                        isDocType = true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            if (!isDocType) {
                reader.reset();
                return preChar;
            }
            boolean hasSquare = false;
            for (; isDocType && nc != 0; ) {//seek the DOCTYPE close
                nc = nextChar(reader);
                hasSquare = hasSquare || nc == '[';
                if (hasSquare) {
                    boolean squareEnd = false;
                    while (nc != 0) {
                        nc = nextChar(reader);
                        if (nc == ']') {
                            squareEnd = true;
                            break;
                        }
                    }
                    if (squareEnd) {
                        while (nc != 0) {
                            nc = nextChar(reader);
                            if (nc == '>') {//DOCTYPE close
                                break;
                            }
                        }
                    }
                }
                if (nc == '>') {//DOCTYPE close
                    break;
                }
            }
            for (; isDocType && nc != 0; ) {//find the tag start
                nc = nextChar(reader);
                if (nc == '<') {
                    nc = nextChar(reader);
                    break;
                }
            }
            return nc;
        }

        char skipComment(BufferedReader reader, char preChar) throws Exception {
            boolean isComment = false;
            reader.mark(10);
            char nc = nextChar(reader);
            if (nc == '-') {
                nc = nextChar(reader);
                if (nc == '-') {// is xml comment
                    isComment = true;
                }
            }
            if (!isComment) {
                reader.reset();
                return preChar;
            }
            for (; isComment; ) {//seek the comment close
                nc = nextChar(reader);
                if (nc == '-') {
                    nc = nextChar(reader);
                    if (nc == '-') {
                        nc = nextChar(reader);
                        if (nc == '>') {// comment end
                            break;
                        }
                    }
                }
            }
            for (; isComment; ) {//find the tag start
                nc = nextChar(reader);
                if (nc == 0) {
                    return 0;
                }
                if (nc == '<') {
                    nc = nextChar(reader);
                    break;
                }
            }
            return nc;
        }

        char skipXmlDeclare(BufferedReader reader, char preChar) throws Exception {
            boolean skipDone = false;
            char c = 0;
            for (; ; ) {
                c = nextChar(reader);
                if (c == 0) {
                    return 0;
                }
                if (c == '?' && nextChar(reader) == '>') {//find xml declare ending
                    skipDone = true;
                    break;
                }
            }
            for (; skipDone; ) {//find the tag start
                c = nextChar(reader);
                if (c == 0) {
                    return 0;
                }
                if (c == '<') {
                    c = nextChar(reader);
                    break;
                }
            }
            return c;
        }
    }

    class TagEndParser extends InnerParser {//END <tag > or <tag />

        int parse(BufferedReader reader) throws Exception {
            if (remain != '/' && remain != '>') {
                char nc = nextChar(reader);
                while (true) {// find the tag open end
                    if (nc == 0) {
                        return 0;
                    }
                    if (nc == '/' || nc == '>') {
                        remain = nc;
                        break;
                    }
                    nc = nextChar(reader);
                }
            }
            if (remain == '/') {
                char nc = nextChar(reader);
                remain = nc;
                if (nc == '>') {
                    node.attr("@done", true);
                    ////System.out.println("==tag closed1== </" + node.tag() + ">");
                    node = current = node.pid() == null ? current : idMap.get(node.pid());
                    return -4;
                } else {//
                    while (true) {
                        nc = nextChar(reader);
                        if (nc == '>') {
                            node.attr("@done", true);
                            ////System.out.println("==tag closed1== </" + node.tag() + ">");
                            node = current = node.pid() == null ? current : idMap.get(node.pid());
                            break;
                        } else if (nc == 0) {
                            node.attr("@error", "unknown '/'");
                            ////System.out.println("==tag error== <" + node.tag() + ">");
                            break;
                        }
                    }
                    return -3; // return AttrParser
                }
            } else {
                ////System.out.println("==tag open end== <" + node.tag() + ">");
            }
            remain = 0;
            return 0;
        }
    }

    class AttrParser extends InnerParser {
        int parse(BufferedReader reader) throws Exception {
            for (; ; ) {// scan all attrs
                if (remain == '/' || remain == '>') {//has no attr
                    return 0;
                }
                StringBuilder name = new StringBuilder();
                StringBuilder value = new StringBuilder();
                StringBuilder[] kv = new StringBuilder[]{name, value};
                int r = remain, valueBlock = -1;
                remain = 0;
                do {
                    char c = (char) r;
                    if (kv[0].length() < 1) {
                        if (c == 0 || c == ' ' || c == '\t' || c == '\n' || c == '\r') {//split blank char
                            continue;
                        }
                    }
                    if (kv[0] == name && (c == '/' || c == '>')) {
                        remain = c;
                        break;
                    }
                    if (c == '=') {
                        kv[0] = value;
                        kv[1] = name;
                    } else {
                        if (kv[0].length() < 1 && kv[0] == value && (c == '"' || c == '\'')) {//attribute start
                            //FIXME: <tag a=1></tag>
                            //FIXME: <tag a></tag>
                            valueBlock = r;
                            continue;
                        }
                        if (kv[0] == value && r == valueBlock) {//attribute end
                            break;
                        }
                        kv[0].append(c);
                    }
                } while ((r = reader.read()) > -1);
                //process escape char
                if (name.length() > 0) {
                    node.attr(name.toString(), escapeToChar(value).toString());
                    ////System.out.println("==attr== " + name + "=" + value);
                }
            }// scan all attrs-END
        }
    }

    class ValParser extends InnerParser {
        int parse(BufferedReader reader) throws Exception {
            List<CharSequence> list = new ArrayList<>();
            StringBuilder value = new StringBuilder();
            StringBuilder tmp = new StringBuilder();
            int r = remain;
            remain = 0;
            boolean cdata = false;//<![CDATA[   ]]>
            do {
                char c = (char) r;
                if (value.length() < 1) {
                    if (c == 0 || c == ' ' || c == '\t' || c == '\n' || c == '\r') {//split blank char
                        continue;
                    }
                }
                if (!cdata && c == '<') {
                    int markLen = tmp.length();
                    tmp.append(c);
                    reader.mark(10);
                    char nc = nextChar(reader);
                    if (nc == '!') {
                        tmp.append(nc);
                        nc = nextChar(reader);
                        if (nc == '[') {
                            tmp.append(nc);
                            nc = nextChar(reader);
                            if (nc == 'C') {
                                tmp.append(nc);
                                nc = nextChar(reader);
                                if (nc == 'D') {
                                    tmp.append(nc);
                                    nc = nextChar(reader);
                                    if (nc == 'A') {
                                        tmp.append(nc);
                                        nc = nextChar(reader);
                                        if (nc == 'T') {
                                            tmp.append(nc);
                                            nc = nextChar(reader);
                                            if (nc == 'A') {
                                                tmp.append(nc);
                                                nc = nextChar(reader);
                                                if (nc == '[') {//start of cdata block
                                                    tmp.setLength(markLen);
                                                    if (value.length() > 0) {
                                                        list.add(value);
                                                        value = new StringBuilder();
                                                    }
                                                    cdata = true;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    // if not cdata and c = <, means value end, new node start...
                    if (!cdata) {
                        reader.reset();// reset reader after <
                        tmp.setLength(markLen);
                        value.append(tmp);
                        list.add(value);
                        StringBuilder sb = new StringBuilder();
                        for (CharSequence cs : list) {
                            if (cs instanceof StringBuilder) {
                                sb.append(escapeToChar((StringBuilder) cs));
                            } else {
                                sb.append(cs);
                            }
                        }
                        if (sb.length() > 0) {
                            node.value(sb.toString());
                            ////System.out.println("==value== " + sb);
                        }
                        remain = c;//NOTICE: this is important
                        break;
                    }
                } else if (cdata && c == ']') {// test cdata end ]]>
                    tmp.append(c);
                    char nc = nextChar(reader);
                    if (nc == ']') {
                        int markLen = tmp.length();
                        tmp.append(nc);
                        nc = nextChar(reader);
                        if (nc == '>') {//end of cdata block
                            tmp.setLength(markLen);
                            if (tmp.length() > 0) {
                                list.add(tmp.toString());
                                tmp = new StringBuilder();
                            }
                            cdata = false;
                        }
                    }
                } else if (cdata) {
                    tmp.append(c);
                } else {
                    value.append(c);
                }
            } while ((r = reader.read()) > -1);
            return 0;
        }
    }

    char nextChar(BufferedReader reader) throws Exception {
        int r = reader.read();
        if (r > -1) {
            return (char) r;
        }
        return (char) 0;
    }

    StringBuilder escapeToChar(StringBuilder value) {
        if (value.indexOf("&") < 0) {
            return value;
        }
        StringBuilder newVal = new StringBuilder();
        int len = value.length();
        for (int i = 0; i < len; i++) {
            int marker = 0;
            char c0 = value.charAt(i);
            if (c0 != '&') {
                newVal.append(c0);
            } else {
                char c1 = (i + 1) < len ? value.charAt(i + 1) : 0;
                char c2 = (i + 2) < len ? value.charAt(i + 2) : 0;
                char c3 = (i + 3) < len ? value.charAt(i + 3) : 0;
                char c4 = (i + 4) < len ? value.charAt(i + 4) : 0;
                char c5 = (i + 5) < len ? value.charAt(i + 5) : 0;
                if (c5 == ';') {
                    String s = new String(new char[]{c0, c1, c2, c3, c4, c5});
                    if ("&nbsp;".equals(s) || "&#160;".equals(s)) {
                        newVal.append(' ');
                        marker = 5;
                    }
                }
                if (marker == 0 && c4 == ';') {
                    String s = new String(new char[]{c0, c1, c2, c3, c4});
                    marker = 4;
                    if ("&amp;".equals(s) || "&#38;".equals(s)) {
                        newVal.append('&');
                    } else if ("&quot;".equals(s) || "&#34;".equals(s)) {
                        newVal.append('"');
                    } else if ("&#62;".equals(s)) {
                        newVal.append('>');
                    } else if ("&#60;".equals(s)) {
                        newVal.append('<');
                    } else {
                        marker = 0;
                    }
                }
                if (marker == 0 && c3 == ';') {
                    String s = new String(new char[]{c0, c1, c2, c3});
                    marker = 3;
                    if ("&gt;".equals(s) || "&#62;".equals(s)) {
                        newVal.append('>');
                    } else if ("&lt;".equals(s) || "&#60;".equals(s)) {
                        newVal.append('<');
                    } else {
                        marker = 0;
                    }
                }
                if (marker == 0) {
                    newVal.append(c0);
                } else {
                    i = i + marker;
                }
            }//end if: c0 != '&'
        }
        return newVal;
    }
}
